﻿using System;
using System.Collections.Generic;
using Engine.Mathematics;
using log4net;

namespace Engine
{
    /// <summary>
    /// this class represent two dimensional square lattice
    /// </summary>
    [Serializable]
    public class Square : Lattice
    {
        #region Members

        private const bool CONDUCT = true;
        private const int dimension = 2;
        private const bool INSULATE = false;
        private static readonly ILog logger = LogManager.GetLogger(typeof (Square));
        private readonly Random random;
        /// <summary>
        /// Conductance of one bond.
        /// </summary>
        private double _bondConductance = 1;

        /// <summary>
        /// We sets this field in case of nonlinear conductance
        /// </summary>
        private NonLinearConductance _bondNonLinearConductance;


        /// <summary>
        /// all bonds in lattice 
        /// </summary>
        private IList<Bond> _bonds;
        private IList<Point> _points;
        private IList<Point> _startPoints;
        private int _cuttedBonds;
        private int cuttedPoints;
        private int xlength;
        private int ylength;

        #endregion

        #region Properties

        /// <summary>
        /// Number of cutted point
        /// </summary>
        public override int CuttedPoints
        {
            get { return cuttedPoints; }
        }

        /// <summary>
        /// Number of working points
        /// </summary>
        public override int WorkingPoints
        {
            get { return _points.Count; }
        }

        /// <summary>
        /// Number of cutted bonds
        /// </summary>
        public override int CuttedBonds
        {
            get { return _cuttedBonds; }
        }

        /// <summary>
        /// Number of working bonds
        /// </summary>
        public override int WorkingBonds
        {
            get { return _bonds.Count; }
        }
        
        /// <summary>
        /// Number of elements in x direction
        /// </summary>
        public override int X
        {
            get { return xlength; }
            set
            {
                xlength = value;
                Reset();
            }
        }

        /// <summary>
        /// Number of elements in y direction
        /// </summary>
        public override int Y
        {
            get { return ylength; }
            set
            {
                ylength = value;
                Reset();
            }
        }

        /// <summary>
        /// Number of elements in z direction
        /// Note. This mehtod is not supported. Because we have 2d implementation.
        /// </summary>
        public override int Z
        {
            get { throw new NotImplementedException("this lattice not support this property"); }
            set { throw new NotImplementedException("this lattice not support this property"); }
        }

        /// <summary>
        ///     
        /// </summary>
        /// <see cref="Lattice.Dimension"/>
        public override int Dimension
        {
            get { return dimension; }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <see cref="Lattice.Points"/>
        public override IList<Point> Points
        {
            get { return _points; }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <see cref="Lattice.Bonds"/> 
        public override IList<Bond> Bonds
        {
            get { return _bonds; }
        }

        /// <summary>
        ///  Conductance of lattice
        /// </summary>
        /// <see cref="Lattice.Conductance"/>
        public override double Conductance
        {
            get { return CalculateConductance(); }
        }

        /// <summary>
        /// Sets bonds non linear conductance
        /// </summary>
        public NonLinearConductance NonLinearConductance {
            set
            {
                _bondNonLinearConductance = value;
            }         
        }

        /// <summary>
        /// Conductance of one bond.
        /// </summary>
        public double BondConductance
        {
            get { return _bondConductance; }
            set
            {
                _bondConductance = value;
                //change conductance of all elements in lattice
                ChangeBondConductance(value);
            }
        }

        #endregion

        /// <summary>
        /// Create new square lattice with size 10*10
        /// </summary>
        public Square()
        {
            xlength = 10;
            ylength = 10;
            random = new Random();
        }

        #region Methods

        /** <summary>Create object model for our lattice.</summary> */
        private void CreateLattice()
        {
            _bonds = new List<Bond>();
            _points = new List<Point>();
            _startPoints = new List<Point>();
            //linking in y direction.
            IList<Bond> oldVerticalBond = new List<Bond>(xlength);
            for (int y = 0; y < ylength; y++)
            {
                //linking in x direction.
                Bond oldBond = null;
                IList<Bond> newOldVerticalBond = new List<Bond>();
                for (int x = 0; x < xlength; x++)
                {
                    var point = new Point(x, y);
                    if (oldBond != null)
                    {
                        oldBond.EndPoint = point;
                    }
                    if (x != xlength - 1)
                    {
                        var horizontalBond = new Bond();
                        horizontalBond.Conductance = _bondConductance;
                        horizontalBond.StartPoint = point;
                        oldBond = horizontalBond;
                        //we need mark bonds in start of this grid as checked.
                        if (y == 0) horizontalBond.IsChecked = true;
                        _bonds.Add(horizontalBond);
                    }
                    if (y == 0)
                    {
                        //mark the start points as checked.
                        point.IsChecked = true;
                        point.Potential = 0d;
                        _startPoints.Add(point);
                    }
                    else if (y > 0)
                    {
                        oldVerticalBond[x].EndPoint = point;
                    }
                    if (y != ylength - 1)
                    {
                        if (y != 0) _points.Add(point);
                        //then we can add vertical bond
                        var verticalBond = new Bond();
                        verticalBond.Conductance = _bondConductance;
                        verticalBond.StartPoint = point;
                        newOldVerticalBond.Add(verticalBond);
                        _bonds.Add(verticalBond);
                    }
                    else
                    {
                        //You need to set explicetly the final voltage on lattice.
                        point.Potential = 1d;
                        point.IsFinalPoint = true;
                    }
                }
                //copy newOldVeticalBonds to oldVerticalBonds variable
                oldVerticalBond = newOldVerticalBond;
            }
        }

        /// <summary>
        ///This method cut the next random.
        /// </summary>
        public override void CutPoint()
        {
            int size = _points.Count;
            int index = random.Next(size);
            Point pointToCut = _points[index];
            pointToCut.RemovePoint();
            _points.Remove(pointToCut);
            cuttedPoints++;
        }

        ///<summary>
        ///  This method cut the next bond
        ///</summary>
        public override void CutBond()
        {
            int size = _bonds.Count;
            int index = random.Next(size);
            //what i should do with this bond
            Bond bondToCut = _bonds[index];
            bondToCut.DisconnectBond();
            //!TODO - stupid logic at this place
            _bonds.Remove(bondToCut);
            _cuttedBonds++;
        }

        /// <summary>
        /// Reset state of lattice
        /// </summary>
        /// <see cref="Lattice.Reset"/>
        public override void Reset()
        {
            CreateLattice();
            cuttedPoints = 0;
            _cuttedBonds = 0;
        }

        /// <summary>
        /// Object based implementation of IsConduct method.
        /// </summary>
        /// <returns>is lattice conduct</returns>
        public override bool IsConduct()
        {
            //start from seriesPercolationThreashold point
            IList<Point> nextGenetation = new List<Point>(_startPoints);
            while (nextGenetation.Count > 0)
            {
                IList<Point> newNextGeneration = new List<Point>();
                foreach (Point point in nextGenetation)
                {
                    if (point.Y == 98) logger.Debug("we log this");

                    //we need to iterate throw each point.
                    IList<Bond> bonds = point.BondList;
                    foreach (Bond bond in bonds)
                    {
                        //check is this bond checked the seriesPercolationThreashold time.
                        if (!bond.IsChecked)
                        {
                            //mark this bond as checked.
                            bond.IsChecked = true;
                            Point endPoint = point.Navigate(bond);
                            //if this point is not checked we need to add its to newNextGeneration
                            if (endPoint != null)
                            {
                                if (!endPoint.IsChecked && !endPoint.IsFinalPoint)
                                {
                                    endPoint.IsChecked = true;
                                    newNextGeneration.Add(endPoint);
                                }
                                else if (endPoint.IsFinalPoint)
                                {
                                    //prepare lattice for the next calculation
                                    PrepareForNextCheck();
                                    //we need to return true value.
                                    return CONDUCT;
                                }
                            }
                        }
                    }
                }
                if (newNextGeneration.Count == 0)
                {
                    logger.Debug("");
                }
                nextGenetation = newNextGeneration;
            }
            //report that our lattice is beyond conductance threashold.
            return INSULATE;
        }

        ///<summary>As you see in IsConduct method we use IsChecked property
        /// to mark elements as checked. We need to release this flag before the next 
        ///calculation.</summary>
        private void PrepareForNextCheck()
        {
            foreach (Bond bond in _bonds)
            {
                bond.IsChecked = false;
            }
            foreach (Point point in _points)
            {
                point.IsChecked = false;
            }
        }

        /// <summary>
        /// Change conductance of all bonds
        /// </summary>
        /// <param name="conductance">new conductance</param>
        private void ChangeBondConductance(double conductance)
        {
            if (_bonds == null) return;
            foreach (var bond in _bonds)
            {
                bond.Conductance = conductance;
            }
        }

        /// <summary>
        /// We calculate conductance of our lattice.
        /// </summary>
        /// <returns>conductance of our lattice.</returns>
        private double CalculateConductance()
        {
            if (_points == null || _bonds == null)
            {
                throw new InvalidOperationException("We must call CreateLattice() before we call this method.");
            }
            //we use this to track about entities that interested us in
            //calculation conductance
            var A = new DenseMatrix(_points.Count, _points.Count);
            var B = new Vector(_points.Count);
            IList<int> watched = new List<int>();
            //try to check algorithm
            IList<double> x0 = new List<double>();
            foreach (Point point in _points)
            {
                int mainIndex = _points.IndexOf(point);
                //then find all relation with other points
                IList<Bond> bonds = point.BondList;
                double partB = 0;
                double coefficientForPoint = 0;
                if (point.Y == 1)
                {
                    //check if it have any connections with seriesPercolationThreashold row
                    if (point.IsConnectedWithPoint(new Point(point.X, 0))) 
                    {
                        watched.Add(mainIndex);
                    }
                }
                foreach (Bond bond in bonds)
                {
                    Point nextPoint = point.Navigate(bond);
                    if (nextPoint != null)
                    {
                        coefficientForPoint += bond.Conductance;
                        if (nextPoint.Potential == null)
                        {
                            A[mainIndex, _points.IndexOf(nextPoint)] = 0d - bond.Conductance;
                        }
                        else
                        {
                            partB += bond.Conductance*(double) nextPoint.Potential;
                        }
                    }
                }
                B[mainIndex] = partB;
                A[mainIndex, mainIndex] = coefficientForPoint;
            }

            //then we need to solve this equation using GMRES implementation
            var gmres = new GMRES(A, B);
            Vector result = gmres.Solve();
            // TODO take into accout the conductivity of particular bond.
            double totalCurrent = 0;
            foreach (int indexForWatch in watched)
            {
                totalCurrent += _bondConductance * result[indexForWatch];
            }
            return totalCurrent;
        }

        /// <summary>
        /// Calculate conductance of lattice with nonlinear elements
        /// </summary>
        /// <returns></returns>
        public double CalculateNonLinearConductance() {
            if (_points == null || _bonds == null)
            {
                throw new InvalidOperationException("We must call CreateLattice() before we call this method.");
            }
            
            IList<int> watched = new List<int>();
            foreach (Point point in _points)
            {
                int mainIndex = _points.IndexOf(point);
                //then find all relation with other points
                if (point.Y == 1)
                {
                    //check if it have any connections with seriesPercolationThreashold row
                    if (point.IsConnectedWithPoint(new Point(point.X, 0)))
                    {
                        watched.Add(mainIndex);
                    }
                }
            }

            NonLinearGMRES gmres = new NonLinearGMRES(_bondNonLinearConductance, _points, _bonds);
            Vector result = gmres.Solve();
            
            // TODO take into accout the conductivity of particular bond.
            double totalCurrent = 0;
            foreach (int indexForWatch in watched) {
                totalCurrent += _bondNonLinearConductance( result[indexForWatch] ) ;
            }
            return totalCurrent;
        }
        

        #endregion
    }
}